import { Application } from 'egg'
import { Model } from 'sequelize'

export class Repository {
  app: Application
  model: Model<any, any>
  constructor(app: Application, model: Model<any, any> | string) {
    // app.logger.info('Repository.constructor', model)
    this.app = app
    if (!model) throw new Error('need model to create repository.')
    // 如果传入字符串repository名，先查找egg对象的model，如果没有查询到再查找sequelize对象的model
    this.model = typeof model === 'string' ? app.model[model] || app.model.models[model] : model
    if (!this.model.name) throw new Error(`repository model '${model}' not found.`)
    // app.logger.info('Repository.constructor', model, this.model.name)
  }

  static toParse(data: string) {
    if (data && typeof data === 'string') {
      try {
        return JSON.parse(data)
      } catch (err) {
        //
      }
    }
    return data
  }

  static expendInclude(includeArray, app: Application) {
    if (!includeArray) return
    includeArray.forEach(ic => {
      const { model } = ic
      if (!model) throw new Error('need model.')
      if (typeof model === 'string') {
        ic.model = app.model[model] || app.model.models[model]
      }
      if (!ic.model) throw new Error(`model '${model}' not found.`)
      if (ic.include) {
        if (!Array.isArray(ic.include)) ic.include = [ic.include]
        Repository.expendInclude(ic.include, app)
      }
    })
  }

  static toQuery(app, query: { where?, order?, offset?, limit?, attributes?, include?, raw?} = {}) {
    const orderObj = Repository.toParse(query.order)
    const { querySettings } = app.config.settings

    const options = {
      raw: typeof query.raw === 'undefined' || query.raw === null ? true : query.raw,
      offset: query.offset >= 0 ? query.offset : querySettings.offset,
      limit: query.limit >= 0 ? query.limit : querySettings.limit,
      order: orderObj || querySettings.order,
    }
    if (query.where) options['where'] = Repository.toParse(query.where)
    if (query.attributes) options['attributes'] = Repository.toParse(query.attributes)
    if (query.include) {
      const include = Repository.toParse(query.include)
      options['include'] = Array.isArray(include) ? include : [include]
      Repository.expendInclude(options['include'], app)
      // console.log(options.include)
    }
    return query
  }

  get name() {
    // return this.model.getTableName()
    return this.model.name
  }

  async all(query: { raw?} = {}) {
    const options = {}
    if (typeof query.raw === 'undefined' || query.raw === null) options['raw'] = true
    return this.model.findAll(options)
  }

  async count(query: { where?, include?, distinct?, group?, attributes?} = {}) {
    let includeArray = Repository.toParse(query.include)
    if (includeArray && !Array.isArray(includeArray)) includeArray = [includeArray]
    Repository.expendInclude(includeArray, this.app)
    return this.model.count({
      where: Repository.toParse(query.where),
      include: includeArray,
      distinct: query.distinct,
      group: Repository.toParse(query.group),
      attributes: Repository.toParse(query.attributes || query.group),
    })
  }

  async sum(field, query: { where?, distinct?} = {}) {
    return this.model.sum(field, {
      where: Repository.toParse(query.where),
      distinct: query.distinct,
      plain: false,
    })
  }

  // this.app.model.User.update({ data: null }, { where: { id: '2e43cb8b-795a-4563-a0c5-37c37aa1588c' } })

  // const options = {
  //   raw: true,
  //   include: {
  //     model: this.app.model.models.users,
  //     where: { name: 'a' }
  //   }
  // }
  // this.app.model.models.userRoles.find(options).then(results => {
  //   this.app.logger.info(results)
  // })
  async list(query: { where?, order?, offset?, limit?, attributes?, include?, raw?} = {}) {
    const options = Repository.toQuery(this.app, query)
    // this.app.logger.info(options)
    return this.model.findAndCountAll(options)
  }

  async findOne(query: { where?, order?, offset?, attributes?, include?} = {}) {
    const options = Repository.toQuery(this.app, query)
    return this.model.findOne(options)
  }

  async find(id) {
    // this.app.logger.info('Repository.find(id)', id, this.model.name)
    const entity = await this.model.findById(id)
    // this.app.logger.info(entity)
    if (!entity) {
      // throw new Error({ code: 404, message: `${this.model.name} '${id}' not found` })
      throw new Error(`${this.model.name} '${id}' not found`)
    }
    return entity
  }

  async build(options?) {
    return this.model.build()
  }

  async create(data) {
    if (Array.isArray(data)) {
      // return this.model.bulkCreate(data)
      return Promise.all(data.map(id => this.create(id)))
    }

    return this.model.create(data)
  }

  async update(options, updates) {
    if (Array.isArray(options)) {
      return Promise.all(options.map(id => this.update(id, updates)))
    } else if (typeof options === 'object' && options.where) {
      const { where, limit, fields } = options
      if (!where) throw new Error('need where.')
      return this.model.update(updates, {
        where: Repository.toParse(where),
        fields: Repository.toParse(fields),
        limit,
      })
    }

    const entity = await this.model.findById(options)
    if (!entity) {
      // throw new Error({ code: 404, message: `${this.model.name} '${options}' not found` })
      throw new Error(`${this.model.name} '${options}' not found`)
    }
    return entity.update(updates)
  }

  async del(options) {
    if (Array.isArray(options)) {
      return Promise.all(options.map(id => this.del(id)))
    } else if (typeof options === 'object' && options.where) {
      const { where, limit } = options
      if (!where) throw new Error('need where.')
      return this.model.destroy({
        where: Repository.toParse(where),
        limit,
      })
    }

    const entity = await this.model.findById(options)
    if (!entity) {
      // throw new Error({ code: 404, message: `${this.model.name} '${options}' not found` })
      throw new Error(`${this.model.name} '${options}' not found`)
    }
    return entity.destroy()
  }
}
